﻿using System.Data;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using net.entity.framework.cache;
using net.entity.framework.common;
using net.entity.framework.db;
using net.entity.framework.query;

namespace net.entity.framework {
    /// <summary>
    /// This class is responsible to perform the transacted operations.
    /// The status of the TyObjCache should be refreshed once the commit opetation is complete.
    /// </summary>
    internal sealed class GraphCommiter {
        private Graph entityGraph;
        private static readonly GraphCommiter instance = new GraphCommiter();

        public static GraphCommiter GetInstance {
            get { return instance; }
        }

        public void Commit<T>(IEnumerable<T> entities) where T : Entity {
            entityGraph = Graph.Create(entities);
            using (var provider = new DataProvider(DbClient.SqlClient)) {
                foreach (var dt in entityGraph.ToTypeSpecificDataTables()) {
                    using (var tempTable = new TempTable(dt, provider)) {
                        tempTable.UpdateDestination();
                    }
                }
            }
            //using (var provider = new DataProvider(DbClient.SqlClient)) {
            //    foreach (var dt in entityGraph.ToTypeSpecificDataTables())
            //    {
            //        provider.Commit(dt);    
            //    }
            //}

            //EntityCache.Instance.RegisterChanges(entities);
        }
    }

    internal sealed class Graph {
        private readonly Dictionary<int, EntityCollection> items;

        public Graph() {
            items = new Dictionary<int, EntityCollection>();
        }

        public Graph(IEnumerable<Entity> entitiesToCommit) {
            foreach (var entity in entitiesToCommit) {
                var key = entity.TypeId;
                var value = items.LookupOrDefault(key, new EntityCollection());
                value.Add(entity);
                items[key] = value;
            }
        }

        public static Graph Create<T>(IEnumerable<T> entitiesToCommit) where T : Entity {
            var graph = new Graph();
            foreach (var entity in entitiesToCommit) {
                var key = entity.TypeId;
                var value = graph.items.LookupOrDefault(key, new EntityCollection());
                value.Add(entity);
                graph.items[key] = value;
            }
            return graph;
        }

        public IEnumerable<KeyValuePair<int, EntityCollection>> TypeSpecificItems {
            get { return items; }
        }

        public IEnumerable<DataTable> ToTypeSpecificDataTables()
        {
            foreach (var kvp in TypeSpecificItems)
            {
                yield return kvp.Value.ToDataTable(kvp.Key);
            }
        }

        //public List<string> GetBatchCommands() {
        //    var batch = new List<string>();
        //    foreach (var kvp in TypeSpecificItems) {
        //        var typeSpecificBatch = kvp.Value.GetTypeSpecificBatchCommands(kvp.Key);
        //        batch.AddRange(typeSpecificBatch);
        //    }
        //    return batch;
        //}

    }

    internal sealed class EntityCollection {
        private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

        #region Members

        private readonly List<Entity> items;
        private List<QueryRoot> propertiesMetaData;

        #endregion

        #region Properties

        internal IList<Entity> NewItems {
            get { return items.Where(x => x.IsNew).ToList(); }
        }

        internal IList<Entity> UpdatedItems {
            get { return items.Where(x => x.IsModified).ToList(); }
        }

        internal IList<Entity> DeletedItems {
            get { return items.Where(x => x.IsDeleted).ToList(); }
        }

        #endregion

        #region Constructors

        internal EntityCollection() {
            items = new List<Entity>();
        }

        #endregion

        #region Methods

        internal void Add(Entity entity) {
            items.Add(entity);
        }

        internal List<string> GetTypeSpecificBatchCommands(int typeId) {
            var batch = new List<string>();
            batch.AddRange(GetInserts(typeId));
            batch.AddRange(GetUpdates(typeId));
            batch.AddRange(GetDeletes(typeId));
            return batch;
        }

        #region Approach B
        public DataTable ToDataTable(int typeId) {
            var dt = GetBlankDataTable(typeId);
            AddNew(dt);
            AddUpdated(dt);
            AddDeleted(dt);
            return dt;
        }

        private DataTable GetBlankDataTable(int typeId)
        {
            var entityType = TypeIdCache.GetValue(typeId).EntityType;
            var dt = new DataTable {
                                       TableName = entityType.Name
                                   };
            propertiesMetaData = new List<QueryRoot>(GetProperties(typeId));
            foreach (var root in propertiesMetaData)
            {
                if (root is Terminal)
                {
                    var terminal = (Terminal)root;
                    dt.Columns.Add(terminal.ColumnName, terminal.ValueType);
                }
                else if (root is EqlEntityRef)
                {
                    var eqlEntityRef = (EqlEntityRef)root;
                    dt.Columns.Add(eqlEntityRef.AliasInfo.ColumnPrefix + "_id", typeof(int));
                    dt.Columns.Add(eqlEntityRef.AliasInfo.ColumnPrefix + "_typeId", typeof(int));
                }
            }
            dt.Columns.Add("RowState", typeof(int));
            return dt;
        }

        private void AddNew(DataTable dt)
        {
            foreach (var item in NewItems)
            {
                var row = dt.NewRow();
                var values = Serialize(item);
                foreach (var kvp in values) {
                    row[kvp.Key] = kvp.Value;
                }
                row["RowState"] = 0;
                dt.Rows.Add(row);
            }
        }

        private void AddUpdated(DataTable dt)
        {
            foreach (var item in UpdatedItems)
            {
                var row = dt.NewRow();
                var values = Serialize(item);
                foreach (var kvp in values)
                {
                    row[kvp.Key] = kvp.Value;
                }
                row["RowState"] = 1;
                dt.Rows.Add(row);
            }
        }

        private void AddDeleted(DataTable dt)
        {
            foreach (var item in DeletedItems)
            {
                var row = dt.NewRow();
                var values = Serialize(item);
                foreach (var kvp in values)
                {
                    row[kvp.Key] = kvp.Value;
                }
                row["RowState"] = 2;
                dt.Rows.Add(row);
            }
        }

        private List<KeyValuePair<string,object>> Serialize(Entity newItem)
        {
            //NOTE: I believe i can do a lot here, this needs to be minimised
            var values = new List<KeyValuePair<string,object>>();
            foreach (var metaData in propertiesMetaData)
            {
                if (metaData is Terminal)
                {
                    var terminal = (Terminal)metaData;
                    var value = newItem.GetPropertyValue<object>(terminal.PropertyInfo, true);
                    values.Add(new KeyValuePair<string, object>(terminal.ColumnName,value));
                }
                else if (metaData is EqlEntityRef)
                {
                    //NOTE: this is a problem, what if the EntityRef is new and the Id and TypeId are not set.
                    var eqlEntityRef = (EqlEntityRef)metaData;
                    var value = newItem.GetPropertyValue<EntityRef>(eqlEntityRef.TargetProperty, true);
                    values.Add(new KeyValuePair<string, object>(eqlEntityRef.AliasInfo.ColumnPrefix+"_id", value.Id));
                    values.Add(new KeyValuePair<string, object>(eqlEntityRef.AliasInfo.ColumnPrefix + "_typeId", value.TypeId));
                }
            }
            return values;
        }

        #endregion

        #region Approach A
        private List<string> GetInserts(int typeId)
        {
            propertiesMetaData = new List<QueryRoot>(GetProperties(typeId));
            var columns = new List<string>();
            foreach (var root in propertiesMetaData)
            {
                if (root is Terminal)
                {
                    var terminal = (Terminal)root;
                    columns.Add(terminal.ColumnName);
                }
                else if (root is EqlEntityRef)
                {
                    var eqlEntityRef = (EqlEntityRef)root;
                    columns.Add(eqlEntityRef.AliasInfo.ColumnPrefix + "_id");
                    columns.Add(eqlEntityRef.AliasInfo.ColumnPrefix + "_typeId");
                }
            }
            var inserts = new List<string>();
            var type = TypeIdCache.GetValue(typeId).EntityType;
            foreach (var item in NewItems)
            {
                var values = SerializeForInsert(item);
                var strColumns = string.Join(", ", columns.ToArray());
                var strValues = string.Join(", ", values.ToArray());
                inserts.Add(string.Format("INSERT INTO {0} ({1}) VALUES({2})", type.Name, strColumns, strValues));
            }

            return inserts;
        }

        private List<string> GetUpdates(int typeId)
        {
            var updates = new List<string>();
            var type = TypeIdCache.GetValue(typeId).EntityType;

            foreach (var item in UpdatedItems)
            {
                var values = SerializeForUpdate(item);
                var strValues = string.Join(",\n", values.ToArray());
                updates.Add(string.Format("UPDATE {0} SET {1} WHERE Id = {2}", type.Name, strValues, item.Id));
            }

            return updates;
        }

        private List<string> GetDeletes(int typeId)
        {
            var deletes = new List<string>();
            var type = TypeIdCache.GetValue(typeId).EntityType;

            foreach (var item in DeletedItems)
            {
                deletes.Add(string.Format("UPDATE {0} SET IsDeleted = 1 WHERE Id = {1}", type.Name, item.Id));
            }

            return deletes;
        }

        private List<string> SerializeForInsert(Entity newItem)
        {
            var values = new List<string>();
            foreach (var metaData in propertiesMetaData)
            {
                if (metaData is Terminal)
                {
                    var terminal = (Terminal)metaData;
                    var value = newItem.GetPropertyValue<object>(terminal.PropertyInfo, true);
                    values.Add(SqlUtil.Literal(value));
                }
                else if (metaData is EqlEntityRef)
                {
                    //NOTE: this is a problem, what if the EntityRef is new and the Id and TypeId are not set.
                    var eqlEntityRef = (EqlEntityRef)metaData;
                    var value = newItem.GetPropertyValue<EntityRef>(eqlEntityRef.TargetProperty, true);
                    values.Add(SqlUtil.Literal(value.Id));
                    values.Add(SqlUtil.Literal(value.TypeId));
                }
            }
            return values;
        }

        private List<string> SerializeForUpdate(Entity newItem)
        {
            var values = new List<string>();
            foreach (var metaData in propertiesMetaData)
            {
                if (metaData is Terminal)
                {
                    var terminal = (Terminal)metaData;
                    var value = newItem.GetPropertyValue<object>(terminal.PropertyInfo, true);
                    values.Add(string.Format("{0} = {1}", terminal.ColumnName, SqlUtil.Literal(value)));
                }
                else if (metaData is EqlEntityRef)
                {
                    //NOTE: this is a problem, what if the EntityRef is new and the Id and TypeId are not set.
                    var eqlEntityRef = (EqlEntityRef)metaData;
                    values.Add(string.Format("{0} = {1}", eqlEntityRef.AliasInfo.ColumnPrefix + "_id",
                                             SqlUtil.Literal(eqlEntityRef.Id)));
                    values.Add(string.Format("{0} = {1}", eqlEntityRef.AliasInfo.ColumnPrefix + "_typeId",
                                             SqlUtil.Literal(eqlEntityRef.TypeId)));
                }
            }
            return values;
        } 
        #endregion

        private static IEnumerable<QueryRoot> GetProperties(int typeId) {
            var entityInfo = TypeIdCache.GetValue(typeId);
            //var entityType = entityInfo.EntityType;
            var entityRefType = entityInfo.EntityRefType;
            var eqlEntity = EqlEntityRefCache.LookupOrCreate(entityRefType).EqlEntity;
            var propertyInfos = eqlEntity.GetType().GetPropertyInfos(Flags);


            foreach (var propertyInfo in propertyInfos) {
                if (propertyInfo.PropertyType == typeof (Terminal)) {
                    var terminal = propertyInfo.GetValue<Terminal>(eqlEntity, true);
                    yield return terminal;
                } else if (propertyInfo.PropertyType.IsSubclassOf(typeof (EqlEntityRef))) {
                    yield return propertyInfo.GetValue<EqlEntityRef>(eqlEntity, true);
                }
            }
        }

        #endregion
    }
}